use std::{
borrow::Cow,
cmp,
future::Future,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use futures::FutureExt;
use tokio::time::{Sleep, sleep};
use tower::{retry::Policy, timeout::error::Elapsed};
use bon::Builder;
use crate::Error as CrateError; use crate::adaptive_concurrency::http::HttpError as GenericHttpError;
use reqwest::{Response as ReqwestResponse, StatusCode};
use tracing::{debug, error, warn}; use std::fmt::Debug;
pub enum RetryAction {
Retry(Cow<'static, str>),
DontRetry(Cow<'static, str>),
Successful,
}
pub trait RetryLogic: Clone + Send + Sync + 'static {
type Error: std::error::Error + Send + Sync + 'static;
type Response: Debug;
fn is_retriable_error(&self, error: &Self::Error) -> bool;
fn should_retry_response(&self, _response: &Self::Response) -> RetryAction {
RetryAction::Successful
}
}
#[derive(Clone, Copy, Debug, Default)]
pub enum JitterMode {
None,
#[default]
Full,
}
#[derive(Debug, Clone, Builder)]
pub struct FibonacciRetryPolicy<L> {
remaining_attempts: usize,
previous_duration: Duration,
current_duration: Duration,
jitter_mode: JitterMode,
current_jitter_duration: Duration,
max_duration: Duration,
logic: L,
}
pub struct RetryPolicyFuture<L: RetryLogic> {
delay: Pin<Box<Sleep>>,
policy: FibonacciRetryPolicy<L>,
}
impl<L: RetryLogic> FibonacciRetryPolicy<L> {
pub fn new(
remaining_attempts: usize,
initial_backoff: Duration,
max_duration: Duration,
logic: L,
jitter_mode: JitterMode,
) -> Self {
FibonacciRetryPolicy {
remaining_attempts,
previous_duration: Duration::from_secs(0),
current_duration: initial_backoff,
jitter_mode,
current_jitter_duration: Self::add_full_jitter(initial_backoff),
max_duration,
logic,
}
}
fn add_full_jitter(d: Duration) -> Duration {
if d.as_millis() == 0 {
return Duration::from_millis(0); }
let jitter = (rand::random::<u64>() % (d.as_millis() as u64)) + 1;
Duration::from_millis(jitter)
}
fn advance(&self) -> FibonacciRetryPolicy<L> {
let next_duration: Duration = cmp::min(
self.previous_duration + self.current_duration,
self.max_duration,
);
FibonacciRetryPolicy {
remaining_attempts: self.remaining_attempts - 1,
previous_duration: self.current_duration,
current_duration: next_duration,
current_jitter_duration: Self::add_full_jitter(next_duration),
jitter_mode: self.jitter_mode,
max_duration: self.max_duration,
logic: self.logic.clone(),
}
}
const fn backoff(&self) -> Duration {
match self.jitter_mode {
JitterMode::None => self.current_duration,
JitterMode::Full => self.current_jitter_duration,
}
}
fn build_retry(&self) -> RetryPolicyFuture<L> {
let policy = self.advance();
let delay = Box::pin(sleep(self.backoff()));
debug!(message = "Retrying request.", delay_ms = %self.backoff().as_millis());
RetryPolicyFuture { delay, policy }
}
}
impl<Req, Res, L> Policy<Req, Res, L::Error> for FibonacciRetryPolicy<L>
where
Req: Clone,
L: RetryLogic<Response = Res>, {
type Future = RetryPolicyFuture<L>;
fn retry(&self, _request: &Req, result: Result<&Res, &L::Error>) -> Option<Self::Future> {
match result {
Ok(response) => match self.logic.should_retry_response(response) {
RetryAction::Retry(reason) => {
if self.remaining_attempts == 0 {
error!(
message = "OK/retry response but retries exhausted; dropping request.",
%reason,
);
None
} else {
warn!(message = "Retrying after OK response indicated retry needed.", %reason, );
Some(self.build_retry())
}
}
RetryAction::DontRetry(reason) => {
error!(message = "Not retriable (from response); dropping request.", %reason, );
None
}
RetryAction::Successful => None,
},
Err(service_error) => { if self.remaining_attempts == 0 {
error!(message = "Retries exhausted; dropping request.", error = %service_error, );
return None;
}
if self.logic.is_retriable_error(service_error) {
warn!(message = "Retrying after service error.", error = %service_error, );
Some(self.build_retry())
} else {
error!(
message = "Non-retriable service error (from logic); dropping request.",
error = %service_error,
);
None
}
}
}
}
fn clone_request(&self, request: &Req) -> Option<Req> {
Some(request.clone()) }
}
impl<L: RetryLogic> Unpin for RetryPolicyFuture<L> {}
impl<L: RetryLogic> Future for RetryPolicyFuture<L> {
type Output = FibonacciRetryPolicy<L>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
std::task::ready!(self.delay.poll_unpin(cx));
Poll::Ready(self.policy.clone())
}
}
impl RetryAction {
pub const fn is_retryable(&self) -> bool {
matches!(self, RetryAction::Retry(_))
}
pub const fn is_not_retryable(&self) -> bool {
matches!(self, RetryAction::DontRetry(_))
}
pub const fn is_successful(&self) -> bool {
matches!(self, RetryAction::Successful)
}
}
#[derive(Clone, Debug, Default)]
pub struct DefaultReqwestRetryLogic;
impl RetryLogic for DefaultReqwestRetryLogic {
type Error = GenericHttpError; type Response = ReqwestResponse;
fn is_retriable_error(&self, error: &Self::Error) -> bool {
match error {
GenericHttpError::Transport { .. } => true,
GenericHttpError::Timeout => true,
GenericHttpError::ServerError { status, .. } => StatusCode::from_u16(*status)
.map(|s| s.is_server_error() || s == StatusCode::TOO_MANY_REQUESTS)
.unwrap_or(false),
GenericHttpError::BuildRequest { .. } => false,
GenericHttpError::InvalidRequest { .. } => false,
GenericHttpError::ClientError { .. } => false, }
}
fn should_retry_response(&self, response: &Self::Response) -> RetryAction {
let status = response.status();
if status.is_success() {
RetryAction::Successful
} else if status == StatusCode::TOO_MANY_REQUESTS || status == StatusCode::SERVICE_UNAVAILABLE || status.is_server_error()
{
RetryAction::Retry(Cow::Owned(format!(
"Server responded with status {}",
status
)))
} else if status.is_client_error() {
RetryAction::DontRetry(Cow::Owned(format!(
"Server responded with client error status {}",
status
)))
} else {
warn!(message = "Unhandled response status for retry logic.", %status);
RetryAction::DontRetry(Cow::Owned(format!(
"Server responded with unhandled status {}",
status
)))
}
}
}
#[derive(Debug, Clone)]
pub struct ExponentialBackoff {
current: u64,
base: u64,
factor: u64,
max_delay: Option<Duration>,
}
impl ExponentialBackoff {
pub fn new(base: u64, factor: u64, max_delay: Option<Duration>) -> ExponentialBackoff {
ExponentialBackoff {
current: base,
base,
factor,
max_delay,
}
}
pub const fn from_millis(base: u64) -> ExponentialBackoff {
ExponentialBackoff {
current: base,
base,
factor: 1u64,
max_delay: None,
}
}
pub const fn factor(mut self, factor: u64) -> ExponentialBackoff {
self.factor = factor;
self
}
pub const fn max_delay(mut self, duration: Duration) -> ExponentialBackoff {
self.max_delay = Some(duration);
self
}
pub fn reset(&mut self) {
self.current = self.base;
}
}
impl Iterator for ExponentialBackoff {
type Item = Duration;
fn next(&mut self) -> Option<Duration> {
let duration = if let Some(duration) = self.current.checked_mul(self.factor) {
Duration::from_millis(duration)
} else {
Duration::from_millis(u64::MAX)
};
if let Some(ref max_delay) = self.max_delay {
if duration > *max_delay {
return Some(*max_delay);
}
}
if let Some(next) = self.current.checked_mul(self.base) {
self.current = next;
} else {
self.current = u64::MAX;
}
Some(duration)
}
}
#[derive(Clone, Debug)]
pub struct ExponentialBackoffPolicy<L: RetryLogic> {
attempts_remaining: usize,
backoff_iterator: ExponentialBackoff,
max_total_retry_duration: Option<Duration>, first_attempt_made: bool, logic: L,
jitter_mode: JitterMode,
}
pub struct ExponentialPolicyFuture<L: RetryLogic> {
delay: Pin<Box<Sleep>>,
policy_state_after_delay: ExponentialBackoffPolicy<L>,
}
impl<L: RetryLogic> ExponentialBackoffPolicy<L> {
pub fn new(
max_attempts: usize,
initial_backoff_iterator: ExponentialBackoff, logic: L,
jitter_mode: JitterMode,
max_total_retry_duration: Option<Duration>,
) -> Self {
Self {
attempts_remaining: max_attempts,
backoff_iterator: initial_backoff_iterator,
max_total_retry_duration, first_attempt_made: false,
logic,
jitter_mode,
}
}
fn build_retry_future(&self, delay_duration: Duration) -> ExponentialPolicyFuture<L> {
let mut next_policy_state = self.clone();
next_policy_state.attempts_remaining -= 1;
next_policy_state.first_attempt_made = true;
debug!(
message = "Retrying request with exponential backoff.",
delay_ms = %delay_duration.as_millis(),
attempts_remaining = next_policy_state.attempts_remaining
);
ExponentialPolicyFuture {
delay: Box::pin(sleep(delay_duration)),
policy_state_after_delay: next_policy_state,
}
}
fn apply_jitter(&self, base_duration: Duration) -> Duration {
match self.jitter_mode {
JitterMode::None => base_duration,
JitterMode::Full => {
if base_duration.as_millis() == 0 {
return Duration::from_millis(0);
}
let random_millis = (rand::random::<f64>() * base_duration.as_millis() as f64) as u64;
Duration::from_millis(random_millis)
}
}
}
}
impl<Req, Res, L> Policy<Req, Res, L::Error> for ExponentialBackoffPolicy<L>
where
Req: Clone,
L: RetryLogic<Response = Res>, {
type Future = ExponentialPolicyFuture<L>;
fn retry(&self, _request: &Req, result: Result<&Res, &L::Error>) -> Option<Self::Future> {
if self.attempts_remaining == 0 {
error!(message = "Max retry attempts reached; dropping request.", error_if_any=?result.err());
return None;
}
let should_retry_action = match result {
Ok(response) => self.logic.should_retry_response(response),
Err(service_error) => {
if self.logic.is_retriable_error(service_error) {
RetryAction::Retry(Cow::Borrowed("Service error deemed retriable"))
} else {
RetryAction::DontRetry(Cow::Borrowed("Service error deemed not retriable"))
}
}
};
match should_retry_action {
RetryAction::Retry(reason) => {
let mut current_backoff_iterator = self.backoff_iterator.clone(); let base_delay = match current_backoff_iterator.next() {
Some(delay) => delay,
None => { warn!(message = "Exponential backoff iterator exhausted, but attempts remain. Not retrying.", %reason);
return None;
}
};
let jittered_delay = self.apply_jitter(base_delay);
warn!(message = "Retrying after response/error indicated retry needed.", %reason, base_delay_ms = base_delay.as_millis(), jittered_delay_ms = jittered_delay.as_millis());
let mut next_policy_state = self.clone();
next_policy_state.attempts_remaining = self.attempts_remaining.saturating_sub(1);
next_policy_state.backoff_iterator = current_backoff_iterator;
Some(ExponentialPolicyFuture {
delay: Box::pin(sleep(jittered_delay)),
policy_state_after_delay: next_policy_state,
})
}
RetryAction::DontRetry(reason) => {
error!(message = "Not retriable (from logic); dropping request.", %reason, error_if_any=?result.err());
None
}
RetryAction::Successful => None,
}
}
fn clone_request(&self, request: &Req) -> Option<Req> {
Some(request.clone())
}
}
impl<L: RetryLogic> Unpin for ExponentialPolicyFuture<L> {}
impl<L: RetryLogic> Future for ExponentialPolicyFuture<L> {
type Output = ExponentialBackoffPolicy<L>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
std::task::ready!(self.delay.poll_unpin(cx));
Poll::Ready(self.policy_state_after_delay.clone())
}
}
#[cfg(test)]
mod tests {
use std::{fmt, time::Duration};
use std::error::Error as StdError; use tokio::time;
use tokio_test::{assert_pending, assert_ready_err, assert_ready_ok, task};
use tower::retry::RetryLayer;
use tower_test::{assert_request_eq, mock};
use super::*;
#[tokio::test]
async fn service_error_retry() {
time::pause();
let policy = FibonacciRetryPolicy::new(
5,
Duration::from_secs(1),
Duration::from_secs(10),
SvcRetryLogic,
JitterMode::None,
);
let (mut svc, mut handle) = mock::spawn_layer(RetryLayer::new(policy));
assert_ready_ok!(svc.poll_ready());
let fut = svc.call("hello");
let mut fut = task::spawn(fut);
assert_request_eq!(handle, "hello").send_error(Box::new(Error(true)) as super::CrateError);
assert_pending!(fut.poll());
time::advance(Duration::from_secs(2)).await;
assert_pending!(fut.poll());
assert_request_eq!(handle, "hello").send_response("world");
assert_eq!(fut.await.unwrap(), "world");
}
#[tokio::test]
async fn service_error_no_retry() {
let policy = FibonacciRetryPolicy::new(
5,
Duration::from_secs(1),
Duration::from_secs(10),
SvcRetryLogic,
JitterMode::None,
);
let (mut svc, mut handle) = mock::spawn_layer(RetryLayer::new(policy));
assert_ready_ok!(svc.poll_ready());
let mut fut = task::spawn(svc.call("hello"));
assert_request_eq!(handle, "hello").send_error(Box::new(Error(false)) as super::CrateError);
assert_ready_err!(fut.poll());
}
#[tokio::test]
async fn timeout_error() {
time::pause();
let policy = FibonacciRetryPolicy::new(
5,
Duration::from_secs(1),
Duration::from_secs(10),
SvcRetryLogic,
JitterMode::None,
);
let (mut svc, mut handle) = mock::spawn_layer(RetryLayer::new(policy));
assert_ready_ok!(svc.poll_ready());
let mut fut = task::spawn(svc.call("hello"));
assert_request_eq!(handle, "hello").send_error(Box::new(Elapsed::new()) as super::CrateError);
assert_pending!(fut.poll());
time::advance(Duration::from_secs(2)).await;
assert_pending!(fut.poll());
assert_request_eq!(handle, "hello").send_response("world");
assert_eq!(fut.await.unwrap(), "world");
}
#[test]
fn backoff_grows_to_max() {
let mut policy = FibonacciRetryPolicy::new(
10,
Duration::from_secs(1),
Duration::from_secs(10),
SvcRetryLogic,
JitterMode::None,
);
assert_eq!(Duration::from_secs(1), policy.backoff());
policy = policy.advance();
assert_eq!(Duration::from_secs(1), policy.backoff());
policy = policy.advance();
assert_eq!(Duration::from_secs(2), policy.backoff());
policy = policy.advance();
assert_eq!(Duration::from_secs(3), policy.backoff());
policy = policy.advance();
assert_eq!(Duration::from_secs(5), policy.backoff());
policy = policy.advance();
assert_eq!(Duration::from_secs(8), policy.backoff());
policy = policy.advance();
assert_eq!(Duration::from_secs(10), policy.backoff());
policy = policy.advance();
assert_eq!(Duration::from_secs(10), policy.backoff());
}
#[test]
fn backoff_grows_to_max_with_jitter() {
let max_duration = Duration::from_secs(10);
let mut policy = FibonacciRetryPolicy::new(
10,
Duration::from_secs(1),
max_duration,
SvcRetryLogic,
JitterMode::Full,
);
let expected_fib = [1, 1, 2, 3, 5, 8];
for (i, &exp_fib_secs) in expected_fib.iter().enumerate() {
let backoff = policy.backoff();
let upper_bound = Duration::from_secs(exp_fib_secs);
assert!(
!backoff.is_zero() && backoff <= upper_bound,
"Attempt {}: Expected backoff to be within 0 and {:?}, got {:?}",
i + 1,
upper_bound,
backoff
);
policy = policy.advance();
}
for _ in 0..4 {
let backoff = policy.backoff();
assert!(
!backoff.is_zero() && backoff <= max_duration,
"Expected backoff to not exceed {:?}, got {:?}",
max_duration,
backoff
);
policy = policy.advance();
}
}
#[derive(Debug, Clone)]
struct SvcRetryLogic;
impl RetryLogic for SvcRetryLogic {
type Error = super::CrateError; type Response = &'static str;
fn is_retriable_error(&self, error: &Self::Error) -> bool {
if let Some(specific_error) = error.downcast_ref::<Error>() {
specific_error.0 } else if error.is::<Elapsed>() { true } else {
false }
}
}
#[derive(Debug)]
struct Error(bool);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Test error (retriable: {})", self.0)
}
}
impl StdError for Error {} }